#ifndef EASYASIO_NET_UDP_ENDPOINT_HPP
#define EASYASIO_NET_UDP_ENDPOINT_HPP

#include <asio.hpp>
#include <list>
#include <memory>
#include <functional>
#include <iostream>

struct MsgWrapper
{
    std::string msg_;
    asio::ip::udp::endpoint end_point_;
    MsgWrapper(const std::string& msg, const asio::ip::udp::endpoint& end_point)
        :msg_(msg), end_point_(end_point)
    { }
};
typedef std::shared_ptr<MsgWrapper> MsgWrapperPtr;
class UdpEndpoint;
typedef std::shared_ptr<UdpEndpoint> UdpEndpointPtr;

class UdpEndpoint: public std::enable_shared_from_this<UdpEndpoint>
{
public:
    typedef std::function<void(const UdpEndpointPtr&, const asio::ip::udp::endpoint& remote, const char* buf, size_t len)> MessageCallback;

    UdpEndpoint(asio::io_service& io_service, const char* local_addr, int local_port = 0)
        :io_service_(io_service),
        socket_(io_service_)
    { 
        asio::ip::udp::endpoint listen_endpoint(asio::ip::address::from_string(local_addr), local_port);
        socket_.open(listen_endpoint.protocol());
        socket_.set_option(asio::ip::udp::socket::reuse_address(true));
        socket_.bind(listen_endpoint);
    }

    ~UdpEndpoint() { socket_.close(); }

    void start()
    {
        do_asyn_receive();
    }

    void sendto(const std::string& msg, const char* remote_addr, int remote_port)
    {
        MsgWrapperPtr spmsg = std::make_shared<MsgWrapper>(msg, 
            asio::ip::udp::endpoint(asio::ip::address::from_string(remote_addr), remote_port));
        io_service_.post(std::bind(&UdpEndpoint::sendtoMessageInloop, shared_from_this(), spmsg));
    }

    void sendto(const std::string& msg, const asio::ip::udp::endpoint& endpoint)
    {
        MsgWrapperPtr spmsg = std::make_shared<MsgWrapper>(msg, endpoint);
        io_service_.post(std::bind(&UdpEndpoint::sendtoMessageInloop, shared_from_this(), spmsg));
    }

	void setMessageCallback(const MessageCallback& cb) { msg_callback_ = cb; }

    void join_multicast_group(const char* addr)
    {
        socket_.set_option(
            asio::ip::multicast::join_group(asio::ip::address::from_string(addr)));
    }

private:
    void do_asyn_receive()
    {
        auto& fun = std::bind(
            &UdpEndpoint::handle_receive_from, shared_from_this(), std::placeholders::_1, std::placeholders::_2);
        socket_.async_receive_from(asio::buffer(data_, max_length), sender_endpoint_, fun);
    }

    void sendtoMessageInloop(const MsgWrapperPtr& msgptr)
    {
        bool is_writing = !msg_list_.empty();
        msg_list_.push_back(msgptr);

        if (!is_writing)
        {
            sendToOneMessageInLoop();
        }
    }

    void sendToOneMessageInLoop()
    {
        MsgWrapperPtr send_msg = msg_list_.front();
        auto& fun = std::bind(
            &UdpEndpoint::handle_sendto_complete, shared_from_this(), std::placeholders::_1, std::placeholders::_2);
        socket_.async_send_to(asio::buffer(send_msg->msg_.c_str(), send_msg->msg_.size()), send_msg->end_point_, fun);
    }

    void handle_sendto_complete(const asio::error_code& error, size_t /*bytes_sent*/)
    {
        if (error)
        {
            std::cout << "handle_sendto_complete error: " << error.message() << std::endl;
        }
        msg_list_.pop_front();

        if (!msg_list_.empty())
        {
            sendToOneMessageInLoop();
        }
    }

    void handle_receive_from(const asio::error_code& error, size_t bytes_recvd)
    {
        do_asyn_receive();
        if (error || bytes_recvd <= 0)
        {
            std::cout << "handle_receive_from: " << error.message() << std::endl;
            return;
        }

        msg_callback_(shared_from_this(), sender_endpoint_, data_, bytes_recvd);
    }
private:
    MessageCallback msg_callback_;
    asio::io_service& io_service_;

    asio::ip::udp::socket socket_;
    asio::ip::udp::endpoint sender_endpoint_;
    enum { max_length = 1500 };
    char data_[max_length];
    
    std::list<MsgWrapperPtr> msg_list_;
};

#endif//EASYASIO_NET_UDP_ENDPOINT_HPP